#!/usr/bin/env python
# -*- coding:utf-8 -*-

import os, sys, signal, time
from heapq import heappush, heappop

from jujucore import Jujucore, JujuTransporter, JujuListener, Fifo

from job import *
from action import *
from packet import *

import log

#######################################################################

will_stop = False

def signal_stop( signo, frame ) :
    global  will_stop
    log.debug( "JUJUB catch #%d signal, would stop.", signo )
    will_stop = True
    return
    # signal_stop

#######################################################################

class   Context(object) :
    '''Context -- context '''
    def __init__(self, scheduler) :
        self.scheduler = scheduler
        # __init__()

    def __del__(self) :
        log.debug('Context::__del__()')
        if self.scheduler :
            del self.scheduler
            self.scheduler = None
        # __del__()

    def stop(self) :
        log.debug('Context::stop()')
        if self.scheduler :
            self.scheduler.stop()
            del self.scheduler
            self.scheduler = None
        # __del__()

    # class Context


#######################################################################

class   Server(object) :
    def serve(self, service, request, address_from) :
        '''do some service on request

        response, address_to = serve(PyString service, PyString request, PyString address_from)

        @param service: name of service
        @param request: request to be handled. may be need decode/unpack first, as to application
        @param address_from: where request come from

        @return: a three-tuple of the (service_to, response, address_to). If no response, return None
        '''
        raise NotImplementedError
        # serve()


#######################################################################

class   Action4Channel(object) :
    def __init__(self) :
        self.q_send = []
        self.q_recv = []            # wait for any, including response
        self.q_recv_response = {}   # wait for response identified by respid

        self.q_in = Fifo()          # incoming packet

        self.sending_packid = 0
        # __init__()

    def __del__(self) :
        del self.q_in
        # __del__()

    def addSendAction(self, action) :
        self.q_send.append(action)
        # addSendJob()

    def addRecvAction(self, action) :
        self.q_recv.append(action)
        # addRecvJob()

    def addRecvResponseAction(self, action, respid) :
        if respid in self.q_recv_response :
            self.q_recv_response[respid].append(action)
        else :
            self.q_recv_response[respid] = [action,]
        # addRecvResponseJob()

    def popRecvActionFor(self, respid) :
        if respid in self.q_recv_response :
            q = self.q_recv_response[respid]
            action = q.pop(0)
            if len(q) < 1 :
                del self.q_recv_response[respid]
            return action
        return None
        # resposeFor()

    def popRecvAction(self) :
        if self.q_recv :
            return self.q_recv.pop(0)
        return None
        # popRecvAction()

    def __str__(self) :
        return 'Action4Channel(q-length[send:%d recv:%d response-wait:%d in:%d])' % \
                (len(self.q_send), len(self.q_recv), len(self.q_recv_response), len(self.q_in))
    # class Job4Channel

class   Scheduler(object)  :
    '''Dispatcher -- dispatch message to consumer'''
    def __init__(self) :
        # table-address : address --> channel
        self.__table_address = {}
        # table-channel : channel --> state of channel
        self.__table_channels = {}
        # table-consumer : SERVICE --> server
        self.__table_services = {}

        self.job_list_ready = []
        self.job_list_done =  []
        self.job_list_fail =  []
        self.job_list_run  =  []

        self.action_list_for_channel = {}

        # periodic list
        self.__table_periodics = []
        # __init__()

    def __del__(self) :
        log.debug('Scheduler::__del__()')
        self.unregisterAllServices()
        # __del__()

    def stop(self) :
        log.debug('Scheduler::stop()')
        self.unregisterAllServices()
        # stop()

    ###################################################################

    def dispatch(self, channel_from, record) :
        '''dispatch a record to a consumer

        List of Actions :
         * parse record to get address info (such as SERVICE name)
         * lookup route-table to get the Server who is responsible for the record(service),
            if No Server is responsible for it :
                ???discard the message(record)
                return False
         * feed message to server
         * return True
        '''
        # parse record, to get SERVICE
        if log.debugable :
            if len(record) < 256 :
                log.debug('CHANNEL %s RECV (size=%d) %s', channel_from.address, len(record), `record`)
            else :
                log.debug('CHANNEL %s RECV (size=%d)', channel_from.address, len(record))

        packet = depack(record)
        if not packet : raise RuntimeError, 'invalid record: %s' % record

        if packet.isRequest() :
            service = packet.service
            # find Consumer for the SERVICE
            if service not in self.__table_services :
                raise RuntimeError, 'SERVICE "%s" is NOT registered' % service
                #return  False
            #    for each SERVICE, organize Consumers as a list, we just use the first consumer
            server = self.__table_services[service][0]
            # just ignore, or log it to some where if NOT ok
            service_to, response, address_to = server.serve(service, packet.body, channel_from.address)
            if response is None :
                # No response
                return True
            action = SendAction()
            action.remote_address = address_to
            action.remote_service = service_to
            action.data = response
            action.data_type = ACTION_DATA_TYPE_RESPONSE
            action.data_respid = packet.packid
            self.addSendAction(action, address_to)
            return True

        elif packet.isResponse() :
            address_from = channel_from.address
            if address_from not in self.action_list_for_channel :
                #log.error('action_list_for_channel : %s', str(self.action_list_for_channel))
                raise RuntimeError, 'CHANNEL %s do NOT exists (%s)' % (channel_from.address, `record`)

            actions = self.action_list_for_channel[ address_from ]
            action = actions.popRecvActionFor( packet.respid )
            if action is None :
                #log.debug('q_in PUSH (respid=%d) %s', packet.respid, `packet`)
                actions.q_in.push( packet )
                return True
            #if action is None :
            #    raise RuntimeError, 'CHANNEL %s do NOT response for (%s)' % (channel_from.address, `record`)

            # action callback
            action.done( packet.body )
            # move job to done-list
            job = action.job
            self.job_list_done.append(job)
        else :
            raise RuntimeError, 'cannot handle %s action for record "%s"' % (packet.action, `record`)

        return  True
        # dispatch()

    def passon(self, record, address) :
        '''pass packet on a channel by address

        todo : what to do if no channal registered for address???
        '''
        channel = self.lookupChannel(address)
        if channel is None :
            raise RuntimeError, 'No channel labeled %s' % address
        channel.push(record + TERMINATOR)
        # passon()

    ###################################################################

    def registerService(self, service, server) :
        if not service : raise RuntimeError, 'Dispatcher: invalid service to register'
        if not server : raise RuntimeError, 'Dispatcher: invalid server to register'
        if service in self.__table_services :
            self.__table_services[service].insert(0, server)
        else :
            self.__table_services[service] = [server,]
        # registerService()
    def unregisterService(self, service, server=None) :
        if not service : raise RuntimeError, 'Dispatcher: invalid service to unregister'
        if not server : raise RuntimeError, 'Dispatcher: invalid server to unregister'
        if service in self.__table_services :
            if server is None :
                del self.__table_services[service]
            else :
                self.__table_services[service].remove(server)
                if len(self.__table_services[service]) < 1 :
                    # no server registered for this service any more
                    del self.__table_services[service]
        # unregisterService()
    def unregisterAllServices(self) :
        for service, servers in self.__table_services.iteritems() :
            for server in servers :
                del server
        self.__table_services.clear()
        # unregisterAllServices()

    ###################################################################

    def registerChannel(self, address, channel) :
        self.__table_address[ address ] = channel
        ##log.info('after register channel : #channels=%d #actions4channels=%d', len(self.__table_address), len(self.action_list_for_channel))
        # registerChannel()
    def unregisterChannel(self, address) :
        if not address : return
        if address in self.__table_address :
            channel = self.__table_address[ address ]
            del self.__table_address[ address ]

            if address in self.action_list_for_channel :
                del self.action_list_for_channel[ address ]
        ##log.info('after unregister channel : #channels=%d #actions4channels=%d', len(self.__table_address), len(self.action_list_for_channel))
        # unregisterChannel()
    def lookupChannel(self, address) :
        if address in self.__table_address :
            return self.__table_address[ address ]
        return None
        # lookupChannel()

    ###################################################################

    def registerPeriodic(self, server, periodic) :
        if (not periodic) or (periodic < 1) :
            periodic = 1.0
        when = time.time() + periodic
        heappush(self.__table_periodics, (when, server,periodic))
        # registerPeriodic()
    def unregisterPeriodic(self, server) :
        count = len(self.__table_periodics)
        for i in range(count) :
            w,s,p = heappop(self.__table_periodics)
            if s == server : continue
            heappush(self.__table_periodics, (w, s, p))
        # unregisterPeriodic()

    def loop_periodic(self) :
        now = time.time()
        count = len(self.__table_periodics)
        for i in range(count) :
            w,s,p = heappop(self.__table_periodics)
            if w <= now :
                try :
                    try :
                        s.OnPeriodic()
                    except :
                        nil, t, v, tbinfo = jujucore.compact_traceback()
                        log.error('loop_periodic exception -- (%s:%s %s)', t, v, tbinfo)
                        pass
                finally :
                    w += p
                    heappush(self.__table_periodics, (w, s, p))
            else :
                heappush(self.__table_periodics, (w, s, p))
                break
        # loop_periodic()

    ###################################################################

    def addSendAction(self, action, address) :
        channel = self.lookupChannel(address)
        if channel is None :
            raise RuntimeError, 'No channel for %s' % `address`
        if address not in self.action_list_for_channel :
            self.action_list_for_channel[ address ] = Action4Channel()
        self.action_list_for_channel[ address ].addSendAction( action )
        # addSendAction()

    def addJob(self, job) :
        self.job_list_done.append(job)
        # addJob()

    def scheduleReadyJobs(self) :
        count_job = len(self.job_list_ready)
        if count_job < 1 :
            # no ready jobs
            return
        while count_job > 0 :
            count_job -= 1
            job = self.job_list_ready.pop(0)
            current_action = job.currentAction()
            if current_action.isSendAction() :
                address = current_action.remote_address
                self.addSendAction(current_action, address)
                #self.job_list_run.append(job)
            elif current_action.isRecvAction() :
                address = current_action.remote_address
                channel = self.lookupChannel(address)
                if channel is None :
                    raise RuntimeError, 'No channel for %s' % `address`
                if address not in self.action_list_for_channel :
                    self.action_list_for_channel[ address ] = Action4Channel()
                self.action_list_for_channel[ address ].addRecvAction( current_action )
                #self.job_list_run.append(job)
            elif current_action.isSendRecvAction() :
                address = current_action.remote_address
                channel = self.lookupChannel(address)
                if channel is None :
                    raise RuntimeError, 'No channel for %s' % `address`
                if address not in self.action_list_for_channel :
                    self.action_list_for_channel[ address ] = Action4Channel()
                self.action_list_for_channel[ address ].addSendAction( current_action )
                #self.job_list_run.append(job)
            else :
                ok,crump = job.execute()
                if ok :
                    job.done(crump)
                    self.job_list_done.append(job)
                else :
                    job.fail(crump)
                    self.job_list_fail.append(job)
        # scheduleReadyJob()

    def scheduleDoneJobs(self) :
        count_job = len(self.job_list_done)
        if count_job < 1 :
            # no done jobs
            return
        while count_job > 0 :
            count_job -= 1
            job = self.job_list_done.pop(0)
            action = job.nextAction()
            if action :
                self.job_list_ready.append(job)
            elif action is None :
                # no more action
                del job
            else :
                self.job_list_done.append(job)
        # scheduleDoneJob()

    def scheduleFailJobs(self) :
        count_job = len(self.job_list_fail)
        if count_job < 1 :
            # no failure jobs
            return

        while count_job > 0 :
            count_job -= 1
            job = self.job_list_fail.pop(0)
            del job
        # scheduleFailJob()

    def loop_once(self) :
        self.scheduleFailJobs()
        self.scheduleDoneJobs()
        self.scheduleReadyJobs()

        self.scheduleConsumers()
        self.scheduleProducers()
        # loop_once()

    def countJobs(self) :
        count_ready = len(self.job_list_ready)
        count_done  = len(self.job_list_done)
        count_fail  = len(self.job_list_fail)

        return count_ready + count_done + count_fail
        # countJobs()

    def countRunningJobs(self) :
        return len(self.job_list_run) + len(self.job_list_ready)
        # countRunningJobs()

    def scheduleConsumers(self) :
        for address, actions in self.action_list_for_channel.iteritems() :
            length_q_in = len(actions.q_in)
            if length_q_in < 1 :
                continue

            for i in xrange(length_q_in) :
                one_zero, packet = actions.q_in.pop()
                #log.debug('q_in POP %s', `packet`)
                if packet.isResponse() :
                    action = actions.popRecvActionFor( packet.respid )
                    if action is None :
                        #log.debug('q_in PUSH-AGAIN %s', `packet`)
                        actions.q_in.push( packet )
                        continue
                    #if action is None :
                    #    raise RuntimeError, 'CHANNEL %s do NOT response for (%s)' % (channel_from.address, `record`)

                    # action callback
                    action.done( packet.body )
                    # move job to done-list
                    job = action.job
                    self.job_list_done.append(job)
                else :
                    actions.q_in.push(packet);
        # scheduleConsumers()

    def scheduleProducers(self) :
        #log.debug('action_list_for_channel : %s', str(self.action_list_for_channel))
        for address, actions in self.action_list_for_channel.iteritems() :
            channel = self.lookupChannel(address)
            if channel is None :
                raise RuntimeError, 'No channel for %s' % `address`
            if channel.countProducers() > 0 :
                # this channel has not exhausted, we will check it whether it send-out next time
                continue
            if len(actions.q_send) < 1 :
                # this channel has no more data to send, just ignore it
                continue
            action = actions.q_send[0]
            packet = Packet()
            if ACTION_DATA_TYPE_RESPONSE == action.data_type :
                packet.respid = action.data_respid
                packet.setResponse()
            else :
                packet.setRequest()
            packet.body = action.data
            packet.service = action.remote_service
            record, packid = enpack(packet)
            if log.debugable :
                if len(record) < 256 :
                    log.debug('CHANNEL %s PUSH (packid=%d size=%d)%s', channel.address, packid, len(record), `record`)
                else :
                    log.debug('CHANNEL %s PUSH (packid=%d size=%d)', channel.address, packid, len(record))
            actions.sending_packid = packid
            #log.debug('CHANNEL %s sending packet (id:%d)', channel, actions.sending_packid)
            channel.push(record)
        # scheduleProducers()

    def OnPreSend(self, channel) :
        address = channel.address
        if address in self.action_list_for_channel :
            actions = self.action_list_for_channel[ address ]
            if log.debugable : log.debug(' PRESEND CHANNEL %s STATUS %s', channel.address, actions)
        # OnPreSend()

    def OnPostSend(self, channel) :
        address = channel.address
        if address not in self.action_list_for_channel :
            raise RuntimeError, 'Suspicious Channel %s' % address
        actions = self.action_list_for_channel[ address ]
        if log.debugable : log.debug('POSTSEND CHANNEL %s STATUS %s', channel.address, actions)
        action = actions.q_send.pop(0)
        job = action.job
        if job is None :
            # a standalone action
            return
        if action.isSendAction() :
            # schedule job to done-list
            self.job_list_done.append(job)
        elif action.isSendRecvAction() :
            # schedule action to recv Queue
            # register respid to wait
            #log.debug('CHANNEL %s sending packet (id:%d)', channel, actions.sending_packid)
            #log.debug('Action RecvResponse for %d', actions.sending_packid )
            actions.addRecvResponseAction(action, actions.sending_packid)
            actions.sending_packid = 0
        else :
            pass

        # OnPostSend()

    # class Scheduler

#######################################################################

#######################################################################

class   JuTCPChannel( JujuTransporter ) :
    def __init__(self, address, jucore=None, context=None, socket=None) :
        self.address = address
        self.jucore = jucore
        self.context = context
        self.socket = socket
        JujuTransporter.__init__(self, address, jucore, socket)

        if self.context :
            self.context.scheduler.registerChannel(self.address, self)
        # __init__()

    def OnRecvRecord(self, record):
        self.context.scheduler.dispatch(self, record)
        # OnRecvRecord()
    ##def OnPreSendRecord(self) :
    ##    self.__context.scheduler.OnPreSend(self)
    ##    # OnPreSendRecord()
    def OnPostSendRecord(self) :
        #log.debug('JUJUB CHANNEL %s SEND-DONE.', self.address)
        self.context.scheduler.OnPostSend(self)
        # OnPostSendRecord()

    def OnConnectionLost(self) :
        log.info('JUJUB OnConnectionLost for client "%s".', self.address)
        self.context.scheduler.unregisterChannel(self.address)
        # OnConnectionLost()
    # class JuTCPChannel

class   JuTCPServer( JujuListener ) :
    def __init__(self, port, jucore=None, context=None) :
        self.jucore = jucore
        self.context = context
        JujuListener.__init__(self, port, jucore)
        # __init__()

    def handleReadEvent(self) :
        connect, address = self.socket.accept()
        JuTCPChannel(address, self.jucore, self.context, connect)
        # handleReadEvent()
    # class JuTCPServer

#######################################################################

class   Jujube(object) :
    def __init__(self) :
        scheduler = Scheduler()
        context = Context(scheduler)
        self.context = context

        self.jucore = Jujucore()

        self.__listeners = {}
        # __init__()

    def __del__(self) :
        log.debug('Jujube::__del__()')
        if self.context :
            del self.context
            self.context = None
        # __del__()

    def stop(self) :
        log.debug('Jujube::stop()')
        if self.context :
            self.context.stop()
            del self.context
            self.context = None
        # stop()

    def listenTCP(self, port) :
        if port in self.__listeners : return
        listener = JuTCPServer(port, self.jucore, self.context)
        self.__listeners[port] = listener
        # listenTCP()

    def connectTCP(self, address) :
        JuTCPChannel(address, self.jucore, self.context)
        # connectTCP()

    def addJob(self, job) :
        self.context.scheduler.addJob(job)
        # addJob()

    def registerService(self, service, server) :
        '''register SERVICE'''
        self.context.scheduler.registerService(service, server)
        # registerServer()

    def unregisterService(self, service, server) :
        '''unregister SERVICE'''
        self.context.scheduler.unregisterService(service, server)
        # unregisterServer()

    def registerPeriodic(self, server, periodic) :
        self.context.scheduler.registerPeriodic(server, periodic)
        # registerPeriodic()

    def unregisterPeriodic(self, server) :
        self.context.scheduler.unregisterPeriodic(server)
        # unregisterPeriodic()

    def run(self) :
        # mask signal
        #log.debug('os.name: %s', os.name)
        signal.signal( signal.SIGTERM, signal_stop )
        if 'posix' == os.name :
            signal.signal( signal.SIGQUIT, signal_stop )

        count_tick = 0
        global  will_stop
        try :
            while not will_stop :
                if self.context.scheduler.countRunningJobs() < 1 :
                    if len(self.jucore.map) < 1 : break
                self.jucore.loop(timeout=0.001, count=1)

                count_tick += 1
                if count_tick >= 1000 :
                    count_tick = 0
                    self.context.scheduler.loop_periodic()

                self.context.scheduler.loop_once()
        except KeyboardInterrupt :
            log.debug( "JUJUB interrupt : %s", `sys.exc_info()[1]` )
            pass
        except :
            raise

        #self.__context.scheduler.unregisterAllServices()
        # run()
    # class Jujube

